// Copyright 2016, 2017 OpenConfigd Project.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package config

import (
	"encoding/json"
	"fmt"
	"log"
	"net"
	"os"
	"strconv"
	"text/template"

	"github.com/coreswitch/netutil"
	"github.com/coreswitch/process"
	"github.com/mitchellh/mapstructure"
)

var DhcpProcessList = process.ProcessSlice{}

func prefix2Subnet(subnet string) string {
	p, _ := netutil.ParsePrefix(subnet)
	p.ApplyMask()
	mask := net.IP(net.CIDRMask(p.Length, 32))
	return "subnet " + p.IP.String() + " netmask " + mask.String()
}

func listDomainNameServers(dhcpIpPool *DhcpIpPool) string {
	list := ""
	for pos, server := range dhcpIpPool.Option.DomainNameServersList {
		if pos != 0 {
			list += ", "
		}
		list += server.Server
	}
	return list
}

func listNtpServers(dhcpIpPool *DhcpIpPool) string {
	list := ""
	for pos, server := range dhcpIpPool.Option.NtpServersList {
		if pos != 0 {
			list += ", "
		}
		list += server.Server
	}
	return list
}

func listVoipTftpServers(dhcpIpPool *DhcpIpPool) string {
	list := ""
	for pos, server := range dhcpIpPool.Option.VoipTftpServersList {
		if pos != 0 {
			list += ", "
		}
		list += server.Server
	}
	return list
}

func listSipServers(dhcpIpPool *DhcpIpPool) string {
	// Encoding IP (1).
	list := "1 "
	for pos, server := range dhcpIpPool.Option.SipServersList {
		if pos != 0 {
			list += ", "
		}
		list += server.Server
	}
	return list
}

func listClasslessRoutes(dhcpIpPool *DhcpIpPool) string {
	list := ""
	for pos, route := range dhcpIpPool.Option.ClasslessRoutesList {
		if pos != 0 {
			list += ", "
		}
		prefix, _ := netutil.ParsePrefix(route.Prefix)
		list += strconv.Itoa(prefix.Length)
		for i := 0; i < (prefix.Length+7)/8; i++ {
			list += fmt.Sprintf(", %d", prefix.IP[i])
		}
		nexthop := netutil.ParseIPv4(route.Nexthop)
		list += fmt.Sprintf(", %d, %d, %d, %d", nexthop[0], nexthop[1], nexthop[2], nexthop[3])
	}
	return list
}

func hasFailover(failoverRole string, failoverPeerAddress string) bool {
	if failoverRole != "" && failoverPeerAddress != "" {
		return true
	}
	return false
}

const dhcpConfigTemplateText = `
# Do not edit
# This file is automatically generated from OpenConfigd.
#
ddns-update-style none;
log-facility local7;

option sip-servers code 120 = { integer 8, array of ip-address };
option voip-tftp-servers code 150 = array of ip-address;
option rfc3442-classless-routes code 121 = array of integer 8;
option ms-classless-routes code 249 = array of integer 8;

default-lease-time {{if .Server.DefaultLeaseTime}}{{.Server.DefaultLeaseTime}}{{else}}600{{end}};
max-lease-time {{if .Server.MaxLeaseTime}}{{.Server.MaxLeaseTime}}{{else}}7200{{end}};
{{if .Server.PingCheck}}ping-check true;{{end}}
{{if hasFailover .Pool.FailoverRole .Pool.FailoverPeerAddress}}
failover peer "failover" {
    {{if eq .Pool.FailoverRole "master"}}primary;{{else}}secondary;{{end}}
    address {{.LocalAddr}};
    port 647;
    peer address {{.Pool.FailoverPeerAddress}};
    peer port 647;
    max-response-delay 30;
    max-unacked-updates 10;
    load balance max seconds 3;
{{if eq .Pool.FailoverRole "master"}}
    mclt 180;
    split 255;
{{end}}
}
{{end}}

{{if ne (len .Pool.RangeList) 0}}{{prefix2Subnet .Pool.Subnet}} {
{{if .Pool.DefaultLeaseTime}}    default-lease-time {{.Pool.DefaultLeaseTime}};{{end}}
{{if .Pool.MaxLeaseTime}}    max-lease-time {{.Pool.MaxLeaseTime}};{{end}}
{{if .Pool.GatewayIp}}    option routers {{.Pool.GatewayIp}};{{end}}
{{if .Pool.Option.DomainName}}    option domain-name "{{.Pool.Option.DomainName}}";{{end}}
{{if .Pool.Option.TftpServerName}}    option tftp-server-name "{{.Pool.Option.TftpServerName}}";{{end}}
{{if .Pool.Option.BootfileName}}    option bootfile-name "{{.Pool.Option.BootfileName}}";{{end}}
{{if .Pool.Option.InterfaceMtu}}    option interface-mtu {{.Pool.Option.InterfaceMtu}};{{end}}
{{if .Pool.Option.DhcpServerIdentifier}}    option dhcp-server-identifier {{.Pool.Option.DhcpServerIdentifier}};{{end}}
{{if ne (len .Pool.Option.DomainNameServersList) 0}}    option domain-name-servers {{listDomainNameServers .Pool}};{{end}}
{{if ne (len .Pool.Option.NtpServersList) 0}}    option ntp-servers {{listNtpServers .Pool}};{{end}}
{{if ne (len .Pool.Option.SipServersList) 0}}    option sip-servers {{listSipServers .Pool}};{{end}}
{{if ne (len .Pool.Option.VoipTftpServersList) 0}}    option voip-tftp-servers {{listVoipTftpServers .Pool}};{{end}}
{{if ne (len .Pool.Option.ClasslessRoutesList) 0}}    option rfc3442-classless-routes {{listClasslessRoutes .Pool}};{{end}}
{{if ne (len .Pool.Option.ClasslessRoutesList) 0}}    option ms-classless-routes {{listClasslessRoutes .Pool}};{{end}}
{{if ne .Pool.Option.TimeOffset 0}}    option time-offset {{.Pool.Option.TimeOffset}};{{end}}
    pool {
{{if hasFailover .Pool.FailoverRole .Pool.FailoverPeerAddress}}        failover peer "failover";{{end}}
{{range $j, $w := .Pool.RangeList}}        range {{$w.RangeStartIp}} {{$w.RangeEndIp}};{{end}}
    }
{{range $j, $w := .Pool.HostList}}    host {{$w.HostName}} {
        hardware ethernet {{$w.MacAddress}};
        fixed-address {{$w.IpAddress}};
    }
{{end}}
}

{{end}}
`

func DhcpLocalAddrLookup(ifName string) string {
	addrConfig := configActive.LookupByPath([]string{"interfaces", "interface", ifName, "ipv4", "address"})
	if addrConfig != nil && len(addrConfig.Keys) > 0 {
		prefix, _ := netutil.ParsePrefix(addrConfig.Keys[0].Name)
		return prefix.IP.String()
	}
	return ""
}

func DhcpServerExec(dhcp *Dhcp, poolConfig *DhcpIpPool) {
	// fmt.Println("DhcpServerExec")
	if poolConfig.Interface == "" {
		fmt.Println("Empty interface")
		return
	}
	if len(poolConfig.RangeList) == 0 {
		fmt.Println("Empty range list")
		return
	}
	if poolConfig.Subnet == "" {
		fmt.Println("Empty subnet")
		return
	}
	configFileName := fmt.Sprintf("/etc/dhcp/dhcpd-%s.conf", poolConfig.IpPoolName)
	pidFileName := fmt.Sprintf("/var/run/dhcpd-%s.pid", poolConfig.IpPoolName)
	leaseFileName := fmt.Sprintf("/var/lib/dhcp/dhcpd-%s.leases", poolConfig.IpPoolName)

	f, err := os.Create(configFileName)
	if err != nil {
		log.Println("Create file:", err)
		return
	}
	tmpl := template.Must(template.New("dhcpTemplate").Funcs(template.FuncMap{
		"prefix2Subnet":         prefix2Subnet,
		"listDomainNameServers": listDomainNameServers,
		"listNtpServers":        listNtpServers,
		"listVoipTftpServers":   listVoipTftpServers,
		"listSipServers":        listSipServers,
		"listClasslessRoutes":   listClasslessRoutes,
		"hasFailover":           hasFailover,
	}).Parse(dhcpConfigTemplateText))

	type TemplValue struct {
		Server    *Server
		Pool      *DhcpIpPool
		LocalAddr string
	}
	value := &TemplValue{
		Server: &dhcp.Server,
		Pool:   poolConfig,
	}
	if hasFailover(poolConfig.FailoverRole, poolConfig.FailoverPeerAddress) {
		fmt.Println("XXX FailoverConfig exists")
		value.LocalAddr = DhcpLocalAddrLookup(poolConfig.IpPoolName)
	}
	//tmpl.Execute(f, dhcp.Server)
	tmpl.Execute(f, value)

	if poolConfig.Interface != "" {
		DhcpServerStart(configFileName, pidFileName, leaseFileName, poolConfig.IpPoolName, poolConfig.Interface)
	} else {
		DhcpServerStart(configFileName, pidFileName, leaseFileName, poolConfig.IpPoolName)
	}
}

func DhcpServerStart(config string, pid string, lease string, ifName string, arg ...string) {
	// fmt.Println("DhcpServerStart")

	vrf := ""
	if len(arg) != 0 {
		vrf = arg[0]
	}

	args := []string{"-f", "-d", "-4",
		"-user", "root",
		"-group", "dhcpd",
		"-pf", pid,
		"-cf", config,
		"-lf", lease, ifName}

	proc := process.NewProcess("dhcpd", args...)
	proc.Vrf = vrf
	proc.File = lease
	proc.StartTimer = 3

	DhcpProcessList = append(DhcpProcessList, proc)
	process.ProcessRegister(proc)
}

func DhcpVrf(config *Dhcp) string {
	vrf := ""
	if len(config.Server.DhcpIpPoolList) == 0 {
		return vrf
	}
	poolList := config.Server.DhcpIpPoolList[0]
	return poolList.Interface
}

func DhcpJsonConfig(path []string, str string) error {
	fmt.Println("[dhcp]DhcpJsonConfig")
	var jsonIntf interface{}
	err := json.Unmarshal([]byte(str), &jsonIntf)
	if err != nil {
		return err
	}
	dhcp := &Dhcp{}
	err = mapstructure.Decode(jsonIntf, &dhcp)
	if err != nil {
		return err
	}

	RelayGroupUpdate(dhcp)

	// Kill all of DhcpInstance.
	for _, proc := range DhcpProcessList {
		for index, value := range proc.Args {
			if value == "-pf" {
				pidFile := proc.Args[index + 1]
				err := os.Remove(pidFile)
				if err != nil {
					log.Println("Delete pid file:", err)
				}
				break
			}
		}
		process.ProcessUnregister(proc)
	}
	DhcpProcessList = DhcpProcessList[:0]

	if len(dhcp.Server.DhcpIpPoolList) == 0 {
		fmt.Println("Empty DhcpIpPoolList")
		return nil
	}

	for _, pool := range dhcp.Server.DhcpIpPoolList {
		//fmt.Println("DHCP pool", pool.IpPoolName)
		DhcpServerExec(dhcp, &pool)
	}

	return nil
}

func DhcpExitFunc() {
	for _, proc := range DhcpProcessList {
		for index, value := range proc.Args {
			if value == "-pf" {
				pidFile := proc.Args[index + 1]
				err := os.Remove(pidFile)
				if err != nil {
					log.Println("Delete pid file:", err)
				}
				break
			}
		}
		process.ProcessUnregister(proc)
	}
	DhcpProcessList = DhcpProcessList[:0]
}

func DhcpVrfClear(vrfId int, cfg *VrfsConfig) {
	dhcp := cfg.Dhcp.Server
	for _, pool := range dhcp.DhcpIpPoolList {
		fmt.Println(fmt.Sprintf("[dhcp]DhcpVrfClear:delete dhcp server dhcp-ip-pool %s", pool.Interface))
		ExecLine(fmt.Sprintf("delete dhcp server dhcp-ip-pool %s", pool.Interface))
	}
}

func DhcpVrfSync(vrfId int, cfg *VrfsConfig) {
	dhcp := &cfg.Dhcp.Server
	fmt.Printf("---- DHCP for vrf%d\n", cfg.Id)
	fmt.Println("Auto DHCP config", dhcp)

	if vrfConfig, ok := EtcdVrfMap[vrfId]; ok {
		DhcpVrfClear(vrfId, &vrfConfig)
	}

	for _, pool := range dhcp.DhcpIpPoolList {
		ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s", pool.Interface))
		ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s interface vrf%d", pool.Interface, cfg.Id))
		ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s gateway-ip %s", pool.Interface, pool.GatewayIp))
		ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s subnet %s", pool.Interface, pool.Subnet))
		if pool.DefaultLeaseTime != 0 {
			ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s default-lease-time %d", pool.Interface, pool.DefaultLeaseTime))
		}
		if pool.MaxLeaseTime != 0 {
			ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s max-lease-time %d", pool.Interface, pool.MaxLeaseTime))
		}
		for pos, rng := range pool.RangeList {
			ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s range %d range-start-ip %s", pool.Interface, pos, rng.RangeStartIp))
			ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s range %d range-end-ip %s", pool.Interface, pos, rng.RangeEndIp))
		}
		for _, host := range pool.HostList {
			ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s host %s ip-address %s", pool.Interface, host.HostName, host.IpAddress))
			ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s host %s mac-address %s", pool.Interface, host.HostName, host.MacAddress))
		}
		option := &pool.Option
		if option.DomainName != "" {
			ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s option domain-name %s", pool.Interface, option.DomainName))
		}
		for _, o := range option.DomainNameServersList {
			ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s option domain-name-servers %s", pool.Interface, o.Server))
		}
		for _, o := range option.NtpServersList {
			ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s option ntp-servers %s", pool.Interface, o.Server))
		}
		if option.TftpServerName != "" {
			ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s option tftp-server-name %s", pool.Interface, option.TftpServerName))
		}
		for _, o := range option.VoipTftpServersList {
			ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s option voip-tftp-servers %s", pool.Interface, o.Server))
		}
		for _, o := range option.SipServersList {
			ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s option sip-servers %s", pool.Interface, o.Server))
		}
		for _, o := range option.ClasslessRoutesList {
			ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s option classless-routes %s nexthop %s", pool.Interface, o.Prefix, o.Nexthop))
		}
		if option.TimeOffset != 0 {
			ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s option time-offset %d", pool.Interface, option.TimeOffset))
		}
		if pool.FailoverRole != "" {
			ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s failover-role %s", pool.Interface, pool.FailoverRole))
		}
		if pool.FailoverPeerAddress != "" {
			ExecLine(fmt.Sprintf("set dhcp server dhcp-ip-pool %s failover-peer-address %s", pool.Interface, pool.FailoverPeerAddress))
		}
	}

	fmt.Printf("---- DHCP relay for vrf%d\n", cfg.Id)
	relay := &cfg.Dhcp.Relay
	for _, group := range relay.ServerGroupList {
		ExecLine(fmt.Sprintf("delete dhcp relay server-group %s", group.ServerGroupName))
		ExecLine(fmt.Sprintf("set dhcp relay server-group %s", group.ServerGroupName))
		for _, addr := range group.ServerAddressList {
			ExecLine(fmt.Sprintf("set dhcp relay server-group %s server-address %s", group.ServerGroupName, addr.Address))
		}
	}
	Commit()
}

func DhcpVrfDelete(vrfId int) {
	fmt.Println("DhcpVrfDelete:", vrfId)
	if vrfConfig, ok := EtcdVrfMap[vrfId]; ok {
		DhcpVrfClear(vrfId, &vrfConfig)
	}
	Commit()
}
